use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::trace;

use crate::{BlockId, TransactionId};

mod transaction_impl;

pub use transaction_impl::TransactionState;
pub use transaction_impl::{
    Transaction, TransactionIn, TransactionInPoint, TransactionOut, TransactionOutPoint,
};

#[derive(Error, Debug, PartialEq)]
pub enum TransactionError {
    #[error("Transaction inputs or outputs are empty")]
    TxInOrTxOutEmpty,

    #[error("Transaction output value is negative")]
    TxOutValueNegative,

    #[error("Coinbase script size is invalid")]
    CoinbaseScriptSizeInvalid,

    #[error("The transaction inputs has null outpoint")]
    TxInOutPointNull,

    #[error("Transaction Outpoint Index Out Of Range")]
    TransactionOutpointIndexOutOfRange,

    // can not find public key hash in script
    #[error("Can not find public key hash in script")]
    CanNotFindPublicKeyHashInScript,

    // DeserializationFailed
    #[error("DeserializationFailed")]
    DeserializationFailed(String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TransactionIndexNode {
    belong_to_block: Option<BlockId>,
    spent_by: Vec<TransactionId>, // one output of a transaction is spent by another transaction
}

impl TransactionIndexNode {
    pub fn new(tx: &Transaction) -> Self {
        let outpoint_num = tx.get_outputs().len();
        // let t = Self {
        //     belong_to_block: None,
        //     spent_by: vec![TransactionId::new_null(); outpoint_num],
        // };

        // tracing::trace!(
        //     "+++++<<<<<<<+++++++++>>>>>>>>>>>>>>>>outpoint_num: {}, tx_index_node: {:#?}",
        //     outpoint_num,
        //     &t
        // );

        // t

        Self {
            belong_to_block: None,
            spent_by: vec![TransactionId::new_null(); outpoint_num],
        }
    }

    pub fn new_1(tx: &Transaction, belong_to_block: Option<BlockId>) -> Self {
        let outpoint_num = tx.get_outputs().len();
        // let t = Self {
        //     belong_to_block,
        //     spent_by: vec![TransactionId::new_null(); outpoint_num],
        // };

        // tracing::trace!(
        //     "++++++++++++++>>>>>>>>>>>>>>>> outpoint_num: {}, capacity:{}, tx_id: {}, tx_index_node: {:#?}",
        //     outpoint_num,
        //     t.spent_by.capacity(),
        //     tx.calc_tx_id(),
        //     &t
        // );

        // t

        Self {
            belong_to_block,
            spent_by: vec![TransactionId::new_null(); outpoint_num],
        }
    }

    pub fn get_belong_to_block(&self) -> Option<BlockId> {
        self.belong_to_block
    }

    pub fn set_belong_to_block(&mut self, block_id: BlockId) {
        self.belong_to_block = Some(block_id);
    }

    pub fn get_spent_by(&self) -> &Vec<TransactionId> {
        &self.spent_by
    }

    pub fn set_spent_by(
        &mut self,
        index: usize,
        tx_id: &TransactionId,
    ) -> Result<(), TransactionError> {
        if index >= self.spent_by.capacity() {
            return Err(TransactionError::TransactionOutpointIndexOutOfRange);
        }

        self.spent_by[index] = *tx_id;
        Ok(())
    }

    pub fn set_spent_by_null(&mut self, index: usize) -> Result<(), TransactionError> {
        if index >= self.spent_by.capacity() {
            return Err(TransactionError::TransactionOutpointIndexOutOfRange);
        }

        self.spent_by[index] = TransactionId::new_null();
        Ok(())
    }

    pub fn is_spent(
        &self,
        index: usize,
        try_spent_by_tx: &TransactionId,
    ) -> Result<bool, TransactionError> {
        if index >= self.spent_by.capacity() || index >= self.spent_by.len() {
            return Err(TransactionError::TransactionOutpointIndexOutOfRange);
        }

        trace!("[index-{}] spent by: {:?}", index, self.spent_by[index]);

        Ok(self.spent_by[index] != TransactionId::new_null()
            && self.spent_by[index] != *try_spent_by_tx)
    }
}

pub type TxIndexCache = std::collections::HashMap<TransactionId, TransactionIndexNode>;
